/*
 * VHDaoConverter.java
 * Created on 25 mai 2009
 *
 * VirtualHockey Internet Platform
 * http://code.google.com/p/virtualhockey/
 *
 * Copyright (c) 2005-2009, Jens and Jan Janke (VirtualHockey Project)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see http://www.gnu.org/licenses/.
 */

package com.hockeo.vhweb.server.convert;

import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;

import com.hockeo.vhbase.data.dao.IGEntity;
import com.hockeo.vhbase.data.dao.assoc.GAssociation;
import com.hockeo.vhbase.data.dao.assoc.GNationalAssociation;
import com.hockeo.vhbase.data.dao.club.GClub;
import com.hockeo.vhbase.data.dao.club.GClubStats;
import com.hockeo.vhbase.data.dao.contest.GContest;
import com.hockeo.vhbase.data.dao.contest.GContestElement;
import com.hockeo.vhbase.data.dao.contest.GContestRound;
import com.hockeo.vhbase.data.dao.match.GKOMatch;
import com.hockeo.vhbase.data.dao.match.GMatch;
import com.hockeo.vhbase.data.dao.season.GMatchday;
import com.hockeo.vhbase.data.dao.season.GMoveday;
import com.hockeo.vhbase.data.dao.season.GSeason;
import com.hockeo.vhbase.data.dao.standings.GArchivedStandings;
import com.hockeo.vhbase.data.helper.GTranslationHelper;
import com.hockeo.vhbase.utils.cache.GSimpleCache;
import com.hockeo.vhweb.client.utils.VHJsData;
import com.hockeo.vhweb.client.utils.VHServerException;

/**
 * Service class that converts server side DAOs into {@link VHJsData} instances.
 *
 * @version $Id$
 * @author jjanke
 */
public class VHDaoConverter
{
  private final GSimpleCache<List<VHMethod>>   d_cacheGetterMethods;
  private final GSimpleCache<Method>           d_cacheExpandMethods;
  private final GSimpleCache<Method>           d_cacheEnhanceMethods;
  private final Set<Class<? extends IGEntity>> d_setExpandedDaos;
  private final Set<Class<? extends IGEntity>> d_setEnhancedDaos;

  /**
   * Creates a new standard VHDaoConverter.
   */
  public VHDaoConverter()
  {
    d_cacheGetterMethods = GSimpleCache.getInstance( 50, 0 );
    d_cacheExpandMethods = GSimpleCache.getInstance( 10, 0 );
    d_cacheEnhanceMethods = GSimpleCache.getInstance( 10, 0 );
    d_setExpandedDaos = new HashSet<Class<? extends IGEntity>>();
    d_setEnhancedDaos = new HashSet<Class<? extends IGEntity>>();

    // Add special DAOs that are expanded if encountered as a return type of a getter
    // method
    //
    d_setExpandedDaos.add( GArchivedStandings.class );
    d_setExpandedDaos.add( GAssociation.class );
    d_setExpandedDaos.add( GClub.class );
    d_setExpandedDaos.add( GClubStats.class );
    d_setExpandedDaos.add( GMatchday.class );
    d_setExpandedDaos.add( GMoveday.class );
    d_setExpandedDaos.add( GContest.class );
    d_setExpandedDaos.add( GContestElement.class );
    d_setExpandedDaos.add( GSeason.class );

    // Add special DAOs which should be enhanced with additional data once all properties
    // are copied
    //
    d_setEnhancedDaos.add( GKOMatch.class );
  }

  /**
   * Returns a set of all the DAOs that are accepted as return types of getter methods
   * because they are expanded directly by the converter.
   */
  Set<Class<? extends IGEntity>> getExpandableDaos()
  {
    return d_setExpandedDaos;
  }

  /**
   * Converts the given IGEntity instance into an VHJsData object which can be sent back
   * to the client using GWT RPC.
   *
   * @param src the source object to be converted
   * @return the VHJsData object matching the original source entity
   * @throws VHServerException
   */
  public VHJsData convert( IGEntity src ) throws VHServerException
  {
    return convert( src, null );
  }

  /**
   * Converts the given IGEntity instance into an VHJsData object which can be sent back
   * to the client using GWT RPC.
   *
   * @param src the source object to be converted
   * @param locale the locale to be used for getter methods that return translated text
   * @return the VHJsData object matching the original source entity
   * @throws VHServerException
   */
  public VHJsData convert( IGEntity src, Locale locale ) throws VHServerException
  {
    List<VHMethod> listGetters = d_cacheGetterMethods.get( src.getClass().getName() );
    VHJsData dest = new VHJsData();

    // If we do not yet have all getter methods ready, we have to get them
    //
    if ( listGetters == null )
    {
      listGetters = getGetterMethods( src.getClass() );
      d_cacheGetterMethods.put( src.getClass().getName(), listGetters );
    }

    // Fill the object with the data from the source object
    //
    copyData( src, dest, listGetters, locale );

    return dest;
  }

  /**
   * Converts the given list of IGEntity instances into a list of VHJsData objects which
   * can be sent back to the client using GWT RPC.
   *
   * @param listSrc the list of source objects to be converted
   * @return the VHJsData object matching the original source entity
   * @throws VHServerException
   */
  public List<VHJsData> convert( List<? extends IGEntity> listSrc ) throws VHServerException
  {
    return convert( listSrc, null );
  }

  /**
   * Converts the given list of IGEntity instances into a list of VHJsData objects which
   * can be sent back to the client using GWT RPC.
   *
   * @param listSrc the list of source objects to be converted
   * @param locale the locale to be used for getter methods that return translated text
   * @return the VHJsData object matching the original source entity
   * @throws VHServerException
   */
  public List<VHJsData> convert( List<? extends IGEntity> listSrc, Locale locale ) throws VHServerException
  {
    List<VHJsData> listDest = new ArrayList<VHJsData>();

    for ( IGEntity src : listSrc )
      listDest.add( convert( src, locale ) );

    return listDest;
  }

  /**
   * <p>
   * Determines all publicly methods prefixed by 'get' that do not take any parameters. By
   * default only methods with the following return types are supported:
   * </p>
   *
   * <ul>
   * <li>Primitive types (byte, char, short, int, long, float, double, boolean) and their
   * corresponding wrapper classes.</li>
   * <li>{@link String}</li>
   * <li>{@link Date}</li>
   * <li>{@link BigDecimal} (converted to {@link Double})</li>
   * </ul>
   *
   * <p>
   * All other 'get' methods not having one of the before mentioned return types are
   * ignored.
   * </p>
   *
   * @param cls the class from which to collect invokable 'get' methods
   * @return
   */
  private List<VHMethod> getGetterMethods( Class<? extends IGEntity> cls )
  {
    final List<VHMethod> list = new ArrayList<VHMethod>();

    for ( Method m : cls.getMethods() )
    {
      if ( !VHMethod.isSupported( getExpandableDaos(), m ) )
        continue;

      list.add( new VHMethod( m ) );
    }

    return list;
  }

  /**
   * Copies all properties obtained using the map of get methods into the destination
   * object. Values that are <code>null</code> are ignored.
   *
   * @param src the source object
   * @param dest the destination object
   * @param locale the locale to be used for getter methods that return translated text
   * @param listGetters the list of getter methods to be invoked
   * @throws VHServerException
   */
  private void copyData( IGEntity src, VHJsData dest, List<VHMethod> listGetters, Locale locale )
      throws VHServerException
  {
    for ( VHMethod m : listGetters )
    {
      Serializable val = m.invoke( src, dest, locale );

      // Ignore null values
      //
      if ( val == null )
        continue;

      // If the returned value belongs to a class that is expandable, we have to do the
      // expansion
      //
      if ( d_setExpandedDaos.contains( m.getMethod().getReturnType() ) )
        expand( val, m.getName(), dest, locale );
      else
        dest.set( m.getName(), val );
    }

    // Check if we have to do an enhancement
    //
    if ( isEntityEnhanceable( src ) )
      enhance( src, dest, locale );

    // Finally, add the name of the class into a special type property
    //
    dest.set( VHJsData.PARAM_TYPE, src.getClass().getSimpleName() );
  }

  /**
   * Checks if the given source entity is enhanceable or not.
   */
  private boolean isEntityEnhanceable( IGEntity src )
  {
    // Check if the class is in the set of enhanceable DAOs
    //
    if ( d_setEnhancedDaos.contains( src.getGEntityClass() ) )
      return true;

    // If not, we have to check if it is a sub-class of an enhanceable DAO
    //
    for ( Class<? extends IGEntity> cls : d_setEnhancedDaos )
      if ( cls.isAssignableFrom( src.getGEntityClass() ) )
      {
        // save the class in the set for better performance in subsequent method calls
        //
        d_setEnhancedDaos.add( src.getGEntityClass() );
        return true;
      }

    return false;
  }

  /**
   * Enhances the destination object with additional data possibly retrieved from the
   * source object. This method determines which is the right enhanceDao method to call.
   *
   * @param src the source object which is transformed into a JS object
   * @param dest the prepared destination object to be enhanced
   * @param locale the locale to use for the retrieval of localised data
   * @throws VHServerException
   */
  private void enhance( IGEntity src, VHJsData dest, Locale locale ) throws VHServerException
  {
    Method m = getEnhanceMethod( src.getGEntityClass() );

    try
    {
      m.invoke( this, new Object[] { src, dest, locale } );
    }
    catch ( InvocationTargetException ex )
    {
      throw new VHServerException( ex.getCause() );
    }
    catch ( Exception ex )
    {
      throw new VHServerException( ex );
    }
  }

  /**
   * Returns the enhance method that should be used for the given argument class. If there
   * is no matching method, it is tried to find a method that takes the direct super class
   * as argument etc.
   *
   * @param cls the class for which a matching enhance method should be retrieved
   * @return the matching method
   * @throws VHServerException
   */
  @SuppressWarnings( "unchecked" )
  private Method getEnhanceMethod( Class<? extends IGEntity> cls ) throws VHServerException
  {
    Method m = d_cacheEnhanceMethods.get( cls.getName() );
    if ( m == null )
    {
      try
      {
        m = this.getClass().getDeclaredMethod( "enhanceDao", new Class[] { cls, VHJsData.class, Locale.class } );
        d_cacheEnhanceMethods.put( cls.getName(), m );
      }
      catch ( NoSuchMethodException ex )
      {
        // This exception may be thrown if a sub-class of a class for which an
        // enhanceDao()
        // method exists has been used for the method retrieval. To account for such
        // cases, we relaunch the search with the classes super class.
        //
        Class<?> clsSuper = cls.getSuperclass();
        if ( clsSuper != null && IGEntity.class.isAssignableFrom( clsSuper ) )
          return getEnhanceMethod( (Class<? extends IGEntity>) clsSuper );

        // We did not find a matching super class, let's rethrow the exception
        //
        throw new VHServerException( ex );
      }
      catch ( Exception ex )
      {
        throw new VHServerException( ex );
      }
    }

    return m;
  }

  /**
   * Enhances a {@link GKOMatch}. Adds the contained matches to the generated JS object
   * and adds contest round data.
   *
   * @param komatch the ko match to be enhanced
   * @param dest the object to which to add the properties
   * @param locale locale to be used for retrieving localised values
   * @throws VHServerException
   */
  private void enhanceDao( GKOMatch komatch, VHJsData dest, Locale locale ) throws VHServerException
  {
    boolean fMovedayInfoSet = false;

    // round info
    //
    expandDao( komatch.getContestElement().getContestRound(), "round_", dest, locale );

    // contained matches
    //
    for ( GMatch m : komatch.getMatches() )
    {
      VHJsData jsm = new VHJsData();

      if ( !fMovedayInfoSet )
      {
        expandDao( m.getMatchday().getMoveday(), "moveday_", dest, locale );
        fMovedayInfoSet = true;
      }

      jsm.set( "played", m.isPlayed() );
      jsm.set( "notRequired", m.isNotRequired() );

      if ( m.isPlayed() )
      {
        jsm.set( "decidedByShootout", m.isDecidedByShootout() );
        jsm.set( "goalsHome", m.getNumGoals( komatch.getClubHome() ) );
        jsm.set( "goalsAway", m.getNumGoals( komatch.getClubAway() ) );
      }

      dest.add( jsm );
    }
  }

  /**
   * Expands the given value. The value's class is assignable from one of the classes in
   * the set of expandable DAOs. This method determines which is the right expandDao
   * method to call. The expandDao method will call certain setter methods and add the
   * results as properties to the passed in destination object. The property name is
   * prefixed by the value of "strProperty" followed by an underscore.
   *
   * @param val the DAO to be expanded
   * @param strProperty the name of the property of the original object
   * @param dest the destination object into which to insert the expanded properties
   * @param locale the locale to use for the retrieval of localised data
   * @throws VHServerException
   */
  private void expand( Serializable val, String strProperty, VHJsData dest, Locale locale ) throws VHServerException
  {
    // We are obliged to iterate because the value classes may be instrumented and
    // therefore not directly correspond to one of the classes in the set of expanded DAOs
    //
    for ( Class<? extends IGEntity> cls : d_setExpandedDaos )
      if ( cls.isAssignableFrom( val.getClass() ) )
      {
        Method m = getExpandMethod( cls );

        try
        {
          m.invoke( this, new Object[] { val, strProperty + "_", dest, locale } );
        }
        catch ( InvocationTargetException ex )
        {
          throw new VHServerException( ex.getCause() );
        }
        catch ( Exception ex )
        {
          throw new VHServerException( ex );
        }

        break;
      }
  }

  /**
   * Returns the expand method that should be used for the given argument class. If there
   * is no matching method, it is tried to find a method that takes the direct super class
   * as argument etc.
   *
   * @param cls the class for which a matching expand method should be retrieved
   * @return the matching method
   * @throws VHServerException
   */
  @SuppressWarnings( "unchecked" )
  private Method getExpandMethod( Class<? extends IGEntity> cls ) throws VHServerException
  {
    Method m = d_cacheExpandMethods.get( cls.getName() );
    if ( m == null )
    {
      try
      {
        m = this.getClass().getDeclaredMethod( "expandDao",
            new Class[] { cls, String.class, VHJsData.class, Locale.class } );
        d_cacheExpandMethods.put( cls.getName(), m );
      }
      catch ( NoSuchMethodException ex )
      {
        // This exception may be thrown if a sub-class of a class for which an expandDao()
        // method exists has been used for the method retrieval. To account for such
        // cases, we relaunch the search with the classes super class.
        //
        Class<?> clsSuper = cls.getSuperclass();
        if ( clsSuper != null && IGEntity.class.isAssignableFrom( clsSuper ) )
          return getExpandMethod( (Class<? extends IGEntity>) clsSuper );

        // We did not find a matching super class, let's rethrow the exception
        //
        throw new VHServerException( ex );
      }
      catch ( Exception ex )
      {
        throw new VHServerException( ex );
      }
    }

    return m;
  }

  /**
   * Expands an association and adds some additional properties to the specified
   * destination object.
   *
   * @param assoc the association to be expanded
   * @param strProperty the prefix to be prepended to all additionally inserted properties
   * @param dest the object to which to add the properties
   * @param locale locale to be used for retrieving localised values
   */
  private void expandDao( GAssociation assoc, String strProperty, VHJsData dest, Locale locale )
  {
    dest.set( strProperty + "id", assoc.getId() );
    dest.set( strProperty + "name", assoc.getId( locale ) );
    dest.set( strProperty + "image100Id", assoc.getImage100Id() );

    if ( assoc.instanceOf( GNationalAssociation.class ) )
    {
      dest.set( strProperty + "nationalCupId", assoc.getId() + "0" );
      dest.set( strProperty + "nationalCupId" + VHMethod.SUFFIX_TRANSLATED, GTranslationHelper.get().translate(
          assoc.getId() + "0", locale ) );
    }
  }

  /**
   * Expands an GArchivedStandings object. See the following reference for a description
   * of all parameters.
   *
   * @see VHDaoConverter#expandDao(GAssociation, String, VHJsData, Locale)
   */
  private void expandDao( GArchivedStandings as, String strProperty, VHJsData dest, Locale locale )
  {
    dest.set( strProperty + "rankType", as.getRankType().name() );
    dest.set( strProperty + "rankType" + VHMethod.SUFFIX_TRANSLATED, as.getRankType( locale ) );
    dest.set( strProperty + "numGoalsFor", as.getNumGoalsFor() );
    dest.set( strProperty + "numGoalsAgainst", as.getNumGoalsAgainst() );
    dest.set( strProperty + "goalsDiff", as.getGoalsDiff() );
  }

  /**
   * Expands a club. See the following reference for a description of all parameters.
   *
   * @see VHDaoConverter#expandDao(GAssociation, String, VHJsData, Locale)
   */
  private void expandDao( GClub club, String strProperty, VHJsData dest, Locale locale )
  {
    dest.set( strProperty + "id", club.getId() );
    dest.set( strProperty + "name", club.getName() );
    dest.set( strProperty + "country", club.getCountry() );
    dest.set( strProperty + "country_name", club.getCountry( locale ) );
    dest.set( strProperty + "image032Id", club.getImage032Id() );
    dest.set( strProperty + "image100Id", club.getImage100Id() );

    expandDao( club.getAssociation(), strProperty + "association_", dest, locale );
    expandDao( club.getLeague(), strProperty + "league_", dest, locale );
  }

  /**
   * Expands a club stats object. See the following reference for a description of all
   * parameters.
   *
   * @see VHDaoConverter#expandDao(GAssociation, String, VHJsData, Locale)
   */
  private void expandDao( GClubStats stats, String strProperty, VHJsData dest, Locale locale )
  {
    dest.set( strProperty + "numCupTitles", stats.getNumCupTitles() );
    dest.set( strProperty + "numEU1Titles", stats.getNumEU1Titles() );
    dest.set( strProperty + "numEU2Titles", stats.getNumEU2Titles() );
    dest.set( strProperty + "numLeagueTitles", stats.getNumLeagueTitles() );
    dest.set( strProperty + "numMinorLeagueTitles", stats.getNumMinorLeagueTitles() );
    dest.set( strProperty + "numPromotions", stats.getNumPromotions() );
    dest.set( strProperty + "numRelegations", stats.getNumRelegations() );
  }

  /**
   * Expands a matchday. See the following reference for a description of all parameters.
   *
   * @see VHDaoConverter#expandDao(GAssociation, String, VHJsData, Locale)
   */
  private void expandDao( GMatchday matchday, String strProperty, VHJsData dest, Locale locale )
  {
    dest.set( strProperty + "id", matchday.getId() );
    dest.set( strProperty + "num", matchday.getNum() );

    expandDao( matchday.getMoveday(), strProperty + "moveday_", dest, locale );
    expandDao( matchday.getContestElement(), strProperty + "contestElement_", dest, locale );
  }

  /**
   * Expands a moveday. See the following reference for a description of all parameters.
   *
   * @see VHDaoConverter#expandDao(GAssociation, String, VHJsData, Locale)
   */
  private void expandDao( GMoveday moveday, String strProperty, VHJsData dest, Locale locale )
  {
    dest.set( strProperty + "id", moveday.getId() );

    if ( moveday.getDate() != null )
      dest.set( strProperty + "date", moveday.getDate().getTime() );
  }

  /**
   * Expands a contest. See the following reference for a description of all parameters.
   *
   * @see VHDaoConverter#expandDao(GAssociation, String, VHJsData, Locale)
   */
  private void expandDao( GContest contest, String strProperty, VHJsData dest, Locale locale )
  {
    dest.set( strProperty + "id", contest.getId() );
    dest.set( strProperty + "id" + VHMethod.SUFFIX_TRANSLATED, contest.getId( locale ) );
    dest.set( strProperty + "category", contest.getCategory() );
  }

  /**
   * Expands a contest element. See the following reference for a description of all parameters.
   *
   * @see VHDaoConverter#expandDao(GAssociation, String, VHJsData, Locale)
   */
  private void expandDao( GContestElement element, String strProperty, VHJsData dest, Locale locale )
  {
    dest.set( strProperty + "id", element.getId() );

    expandDao( element.getContestRound(), strProperty + "contestRound_", dest, locale );
    expandDao( element.getContest(), strProperty + "contest_", dest, locale );
  }

  /**
   * Expands a contest round. See the following reference for a description of all
   * parameters.
   *
   * @see VHDaoConverter#expandDao(GAssociation, String, VHJsData, Locale)
   */
  private void expandDao( GContestRound round, String strProperty, VHJsData dest, Locale locale )
  {
    dest.set( strProperty + "id", round.getId() );
    dest.set( strProperty + "type", round.getRoundType().name() );
    dest.set( strProperty + "type" + VHMethod.SUFFIX_TRANSLATED, round.getRoundType( locale ) );
    dest.set( strProperty + "num", round.getRoundNum() );
  }

  /**
   * Expands a season. See the following reference for a description of all parameters.
   *
   * @see VHDaoConverter#expandDao(GAssociation, String, VHJsData, Locale)
   */
  private void expandDao( GSeason season, String strProperty, VHJsData dest, Locale locale )
  {
    dest.set( strProperty + "id", season.getId() );
    dest.set( strProperty + "name", season.getName() );
  }
}
